⚑ Plotly#

This tutorial shows how to build interactive visualizations for energy data using Plotly Express. ✨ Unlike static Matplotlib/Seaborn plots, Plotly charts let you:

  • πŸ” Zoom, pan, and reset views

  • πŸ–±οΈ Hover to see tooltips

  • βœ… Toggle lines and categories in the legend

  • πŸ’Ύ Export to HTML or PNG


Setup & Imports πŸ› οΈ#

# If needed, install Plotly:
# !pip install plotly 
import numpy as np
import pandas as pd
import plotly.express as px
from IPython.display import HTML, display

import plotly.graph_objects as go
from plotly.subplots import make_subplots

# Use a clean white theme
px.defaults.template = "plotly_white"

Interactive Line: Hourly Demand ⏰#

Line plots are the bread and butter of energy time series. With Plotly, you can zoom into hours, hover over values, and explore patterns interactively.

rng = pd.date_range("2024-01-01", periods=24*7, freq="h")
demand = 1200 + 250*np.sin(2*np.pi*(rng.hour/24)) + np.random.normal(0, 40, len(rng))

df = pd.DataFrame({"time": rng, "demand_MW": demand})

fig = px.line(df, x="time", y="demand_MW",
              title="Hourly Electricity Demand (Interactive)")
fig.update_layout(xaxis_title="Time", yaxis_title="Demand [MW]")

display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()

Comparing Weeks πŸ“…#

Overlay multiple weeks and use the legend to toggle lines on/off.

rng = pd.date_range("2024-02-01", periods=24*7, freq="h")
week1 = 1100 + 240*np.sin(2*np.pi*(rng.hour/24)) + np.random.normal(0, 35, len(rng))
week2 = week1 * 1.07 + np.random.normal(0, 20, len(rng))  # ~7% higher

df2 = pd.DataFrame({
    "time": list(rng) * 2,
    "demand_MW": np.r_[week1, week2],
    "week": ["Week 1"] * len(rng) + ["Week 2"] * len(rng)
})

fig = px.line(df2, x="time", y="demand_MW", color="week",
              title="Two Weeks of Demand")
fig.update_layout(xaxis_title="Time", yaxis_title="Demand [MW]")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()

Range Slider & Buttons ⏳#

Add interactive time filters β€” switch between 1 day, 3 days, 1 week, or full range.

fig = px.line(df, x="time", y="demand_MW", title="Demand with Range Slider")
fig.update_xaxes(rangeslider_visible=True,
                 rangeselector=dict(
                     buttons=list([
                         dict(count=24, label="1D", step="hour", stepmode="backward"),
                         dict(count=3,  label="3D", step="day",  stepmode="backward"),
                         dict(count=7,  label="1W", step="day",  stepmode="backward"),
                         dict(step="all")
                     ])
                 ))
fig.update_layout(xaxis_title="Time", yaxis_title="Demand [MW]")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()

Stacked Area: Generation Mix πŸŒžπŸ’¨πŸ’§#

Stacked areas are great for showing supply mix (Solar, Wind, Hydro).

days = pd.date_range("2024-03-01", periods=14, freq="D")
solar = np.clip(200 + 50*np.sin(2*np.pi*(days.dayofyear/365)), 150, 300)
wind  = 300 + 80*np.sin(2*np.pi*(days.dayofyear/14) + 1)
hydro = 500 + np.random.normal(0, 25, len(days))

mix = pd.DataFrame({"day": days, "Solar": solar, "Wind": wind, "Hydro": hydro})
long = mix.melt(id_vars="day", var_name="source", value_name="MWh")

fig = px.area(long, x="day", y="MWh", color="source",
              title="Daily Generation Mix (Stacked Area)")
fig.update_layout(xaxis_title="Day", yaxis_title="Energy [MWh]")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()

Scatter with Color & Size 🎯#

Scatterplots let us show demand vs temperature, with season as color and wind as bubble size.

np.random.seed(0)
n = 500
temp   = np.random.uniform(-15, 30, n)
demand2 = 1250 + 5*(temp-15)**2 + np.random.normal(0, 60, n)
wind   = np.random.uniform(0, 20, n)
season = pd.cut(temp, bins=[-50,0,15,50], labels=["Winter","Spring/Autumn","Summer"])

df_sc = pd.DataFrame({"temp": temp, "demand": demand2, "wind": wind, "season": season})

fig = px.scatter(df_sc, x="temp", y="demand",
                 color="season", size="wind",
                 labels={"temp":"Temperature (Β°C)", "demand":"Demand [MW]", "wind":"Wind speed (m/s)"},
                 title="Demand vs Temperature (Color=Season, Size=Wind)")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()

Correlation Heatmap πŸ”₯#

See which variables move together using a correlation heatmap.

weather = pd.DataFrame({
    "demand":  [2000, 2100, 2200, 1900, 1800, 1700, 1600],
    "temp":    [-5, 0, 5, 10, 15, 20, 25],
    "wind":    [2, 3, 4, 5, 6, 4, 3],
    "humidity":[85, 80, 75, 70, 65, 60, 55],
    "solar":   [50, 100, 200, 400, 600, 750, 850]
})
corr = weather.corr(numeric_only=True)

fig = px.imshow(corr, text_auto=".2f", color_continuous_scale="RdBu_r",
                title="Correlation Heatmap (Demand & Weather)")
fig.update_xaxes(side="bottom")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()

Facets (Small Multiples) πŸ–ΌοΈ#

Break down scatterplots by weekday to see differences.

df_sc["weekday"] = np.random.choice(
    ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"], size=len(df_sc)
)

fig = px.scatter(df_sc, x="temp", y="demand", color="season",
                 facet_col="weekday", facet_col_wrap=4, opacity=0.6,
                 title="Demand vs Temperature by Weekday")
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()

Bar Charts πŸ“Š#

Compare categories (e.g., regions).

regions = ["North","South","East","West"]
demand_r = [1400, 1600, 1200, 1100]
df_bar = pd.DataFrame({"Region": regions, "Demand": demand_r})

fig = px.bar(df_bar, x="Region", y="Demand", text="Demand",
             title=" Regional Electricity Demand")
fig.update_traces(textposition="outside")
fig.update_layout(yaxis_title="Demand [MWh]")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()

Dual-Axis Chart πŸ“ˆπŸ’Ά#

Sometimes we need two scales β€” e.g., demand [MW] vs price [€/MWh].

from plotly.subplots import make_subplots
import plotly.graph_objects as go

# Dates
idx = pd.date_range("2024-04-01", periods=30, freq="D")

# Generate demand as a NumPy array
demand_d = 1300 + 150*np.sin(2*np.pi*(np.arange(len(idx))/7)) + np.random.normal(0, 40, len(idx))

# Now demand_d is an array, so .mean() works
price = 50 + 0.07*(demand_d - demand_d.mean()) + np.random.normal(0, 4, len(idx))

# Dual-axis plot
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(go.Scatter(x=idx, y=demand_d, name="Demand [MW]", mode="lines+markers"), secondary_y=False)
fig.add_trace(go.Bar(x=idx, y=price, name="Price [€/MWh]", opacity=0.5), secondary_y=True)

fig.update_layout(title_text="Dual-Axis: Demand vs Price",
                  bargap=0.2, legend=dict(orientation="h", y=1.1))
fig.update_xaxes(title_text="Day")
fig.update_yaxes(title_text="Demand [MW]", secondary_y=False)
fig.update_yaxes(title_text="Price [€/MWh]", secondary_y=True)

display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()

Wrap-up ✨#

  • Use line/area plots for time series.

  • Use scatter/bubbles for relationships.

  • Use heatmaps/facets for multivariate comparisons.

  • Use dual-axis when two scales matter (but don’t overuse it).

  • More info https://plotly.com/python/plotly-fundamentals/


πŸ“š More Resources#

For more details and advanced usage of Plotly in Python, check out the official documentation:

πŸ”— Plotly Fundamentals Guide